#!/usr/bin/python2.4

# pylint: disable-msg=C6304

"""Reads Wind forecast images from UW."""

import glob
import os
import re
import sys
import time

import imagereader
import matrix
from matrix import DM
from matrix import LatLon
from matrix import Point

import timeformat
import vectorfield
import windbarb


def ReadGIFFromUW(desired):
  """Read all of the .gif files with 4km SFC wind forecast from UW."""

  # TODO(rrh): Add support for other dates for which we might have archived data
  if 'today' in desired or 'now' in desired:
    for uw_http_dir, uw_date_dir in UWTodayDirectoryNames():
      base_fmt = 'ww_wssfc.%02d.0000.gif'
      last_delta_hour = 84  # 84 hours of forecast, on one hour blocks
      imagereader.GetGIFFromServer(
          uw_http_dir, uw_date_dir,
          base_fmt, last_delta_hour)


def UWTodayDirectoryNames():
  """Return a list of directory names for recent model run starts in UTC.

  It is up to the caller to figure out which one of these is most likely
  to be most recent and most complete.

  Returns:
    A list of pairs.  The first item in the pair is the http path name
      to a directory on the server; the actual file in that directory
      must be generated by the caller.
    The second item in the pair is the directory on the local file system
      that is to receive the copied file(s).
  """
  return_values = []
  now_as_if_utc = int(time.time()) + 0 * 3600
  model_run_interval_secs = 12 * 3600
  x = int(now_as_if_utc)  # round down
  last_gmtime_run = int(x / model_run_interval_secs) * model_run_interval_secs

  # Look back over the last 2 runs, done every 12 hours,
  # and (re)grab them all.
  nruns = 4
  for run_delta in xrange(0, nruns):
    uw_date_dir = time.strftime(
        '%Y%m%d%H',
        time.gmtime(last_gmtime_run - 12 * 3600 * run_delta))
    uw_http = (
        'http://www.atmos.washington.edu/mm5rt/data/' +
        uw_date_dir +
        '/images_d3/')
    local_dir_name = os.path.join(
        imagereader.WORKDIR,
        'uw',
        uw_date_dir)
    return_values.append((uw_http, local_dir_name))
  print 'return_values=%s' % (return_values,)
  return return_values


def UWMostRecentCompleteModelRun():
  """Return the full path to a local directory holding the most recent run."""
  for _, local_dir_name in sorted(UWTodayDirectoryNames(), reverse=True):
    full_path_glob = os.path.join(local_dir_name, 'ww_wssfc.*.ppm')
    xlen = len(glob.glob(full_path_glob))
    if True:
      print '%s ==> %d' % (full_path_glob, xlen,)
    # 84 hours is the maximum forecast hours, but we'll take 72 or more
    if xlen >= 72:
      # Got everything
      return local_dir_name
  return full_path_glob  # TODO(rrh): nothing was read


def GetSummaryCSVFileName():
  return os.path.join(UWMostRecentCompleteModelRun(), 'summary.csv')


class UWWRFVectorField(vectorfield.VectorField):

  def __init__(self, name='unknown'):
    super(UWWRFVectorField, self).__init__(name)
    # These deltas are in MINUTES.  They must be large enough so that
    # the starting buoy on the Columbia Bar falls within the extended edges
    # of the bounding box for the UW-WRF 4km grid.
    # The bar is at 46d12m, but the first wind barb from UW-WRF is at 46d18m
    self.SetLatLonFuzzMinutes(9, 9)
    self.SetTimeFuzzSeconds(2 * 3600)  # winds assumed to not change much


class WrappedWRFPixMatrix(object):
  """Wraps a pix map to look for pixels of a particular color."""

  def __init__(self, pix, target=imagereader.RED, alt_target=imagereader.RED):
    self._pix = pix
    self._target = target
    self._alt_target = alt_target

  def GetPixel(self, point):
    """Returns True if the pixel has the target color or the alternate color."""
    pixel = self.GetPixelRaw(point)
    return pixel == self._target or pixel == self._alt_target

  def GetPixelRaw(self, point):
    if point.x < 0 or point.y < 0:
      return None
    if point.x >= 628 or point.y >= 628:
      return None
    pixel = self._pix[point.x, point.y]
    return pixel


class UWImageReader(imagereader.ImageReader):
  """Reads PPM image files from UW holding wind forecast data."""

  def __init__(self, tesseract_api, trace_level=0):
    super(UWImageReader, self).__init__(tesseract_api, trace_level)
    self._is_calibration = False

  def GetSourceFiles(self, desired):
    """Get names of source image files to read.

    Args:
      desired: A standard name describing the desired set of data,
        either 'today' or a UTC model initialization time formatted '%Y%m%d%H'.

    Returns:
      A list of all file names to read.
    """
    if 'today' in desired or 'now' in desired:
      full_path_glob = os.path.join(
          UWMostRecentCompleteModelRun(),
          'ww_wssfc.*.ppm')
      return glob.glob(full_path_glob)
    else:
      return []

  def ImageBoundingBox(self):
    # Crop box tuned for the UW WRF images
    # All these values were determined empirically.
    # DO NOT CHANGE THESE.  There are many other places that know where
    # specific points are in the UW WRF image after this crop operation.
    gutter = 10
    left = 141 - gutter
    upper = 723 + gutter
    right = 749 + gutter
    lower = 115 - gutter
    crop_box = (left, lower, right, upper)  # That's right, this works!
    return crop_box

  def TextBoundingBox(self):
    # The complete text  field is formatted something like:
    # UW WRF-GFS 4km Domain Init:<string with UTC>\
    # Fcst: 19 h Valid: <string with UTC> (<string with PDT>)
    # We're after the UTC value just after the Valid: handle
    if False:
      return (0, 0, 850, 85)  # whole text region; really confuses Tesseract
    if True:
      # Empirical to select the sub image of the Valid string,
      # a string like '13 UTC Fri 29 Apr 11'
      start_x = 425
      width_x = 205
      return (start_x, 20, start_x + width_x, 60)

  def SelectPreferredColor(self, file_name, image):  # pylint: disable-msg=W0613
    return imagereader.SelectPureRed(image)

  def GetImageTime(self, string_scanned, file_name):
    """Convert the string_scanned from UW WRF images OCR into date and time.

    Args:
      string_scanned: the string that was returned by the OCR engine.
      file_name: the name of the image file from which the string was extracted.

    Returns:
      The string that was scanned.
    """
    if True:
      timestamp_filename = self.MakeTimeFromFileName(file_name)
      timestamp = timestamp_filename  # Much more reliable than using OCR.
    else:
      # rrh gave up on getting the OCR path to work.
      timestamp_ocr = self.MakeTimeFromOCR(string_scanned)
      timestamp = timestamp_ocr

    if timestamp:
      self._unix_epoch_time = timestamp
      print 'file_name=%s time_via_fname=%s' % (
          file_name,
          timeformat.FormatTime(timestamp_filename))
      return string_scanned
    else:
      print 'file_name=%s string_scanned=%18s ==> MISS' % (
          file_name,
          string_scanned)
      return None

  def MakeTimeFromFileName(self, file_name):
    """Make a unix time value from information encoded in the file name."""
    basename = os.path.basename(file_name)
    # The file names are of the form ww_wssfc.%02d.%04d.gif
    # %02d is the offset, in hours, from the start of the model run
    # %04d is the ... TODO(rrh): find out what this is
    m = re.match(r'ww_wssfc\.(..)\.(....)\.ppm', basename)
    hours_projected_forward = int(m.group(1))

    # The directory is named something like  2011050300
    dirname = os.path.basename(os.path.dirname(file_name))
    m = re.match(r'(....)(..)(..)(..)', dirname)
    canonical_time_str = '%s-%s-%s %s00' % (
        m.group(1), m.group(2), m.group(3), m.group(4))
    wrf_start_time = timeformat.ScanTimeString(canonical_time_str)
    wrf_start_time -= 7 * 3600  # scanned a string in UTC; convert to equiv PDT

    return wrf_start_time + hours_projected_forward * 3600

  def MakeTimeFromOCR(self, string_scanned):
    """Given string_scanned from Tesseract, return the time encoded therein.

    Args:
      string_scanned: A string from Tesseract.

    Returns:
      The number of seconds since the unix epoch, or None if the string
      could not be scanned.
    """
    # Tesseract doesn't scan the Init: correctly; it drops the :, and bobbles
    #   the last 2 letters.
    # tesseract scans 8's as B's
    # tesseract scans 0's as U's in some cases
    # tesseract scans 0's as O's in some cases
    # tesseract scans 0's as D's in some cases
    d2 = '([0-9OUBD]{1,2})'
    date_re = (
        d2  # hour
        + ' UTC'  # TZ (always UTC)
        + ' (...) '  # day_name
        + d2  # day in month
        + ' ([A-Za-z][a-z][a-z]) '  # month name
        + d2  # last 2 digits of year
        )

    valid_re = date_re

    times = {}
    # This loop is set up to pick out several time strings
    # (Init, Valid.UTC and Valid.PDT), but we only pick out Valid.UTC
    # and do a gross conversion to localtime with the Valid.UTC.
    for time_name, rexpr in [
        ('valid', valid_re),
        ]:
      rex = re.compile(rexpr)
      m = rex.search(string_scanned)
      if m:
        delta_f = 0  # Field indent
        hour = int(imagereader.UnTesseract(m.group(delta_f + 1)))
        # day_name = m.group(delta_f + 2)
        date = int(imagereader.UnTesseract(m.group(delta_f + 3)))
        month = m.group(delta_f + 4)
        year = int(imagereader.UnTesseract(m.group(delta_f + 5)))
        if hour == 29:
          hour = 20  # OCR hack
        if hour == 97:
          hour = 7  # OCR hack
        reformatted = '%d-%02s-%02d %02d:00 %s' % (
            2000 + year,
            month,
            date,
            hour,
            #'UTC'  # gross and pathetic, compensated for later with -7*60*60
            'PDT'
            )
        time_fmt = '%Y-%b-%d %H:%M %Z'
        x = time.strptime(reformatted, time_fmt)
        timestamp = time.mktime(x)
        timestamp -= 7 * 3600  # gross: compensate for PDT to UTC
        parsed = timestamp
        times[time_name] = parsed

    if 'valid' in times:
      return times['valid']
    else:
      return None  # Could not scan the string

  def CustomAnalysis(self, input_file_name, output_file=sys.stdout):
    self.ExploreWindBarbs(input_file_name, output_file=output_file)

  def CustomAnalysisAll(self):
    """Do analysis to determine bounding boxes and the barb's fixed tip.

    NB: this routine was used to determine the contents of FixedWRFPoints,
    and is not normally run.
    """

    # Combine images under Or (to find bounding box of wind barbs)
    or_image = self._images[self._canonical_file_name].copy()
    for file_name in self._file_names:
      print >>sys.stdout, 'Or in %s' % (file_name,)
      sys.stdout.flush()
      imagereader.OrRed(or_image, self._images[file_name])

    if False:
      and_image = self._images[self._canonical_file_name].copy()
      for file_name in self._file_names:
        print >>sys.stdout, 'And in %s' % (file_name,)
        sys.stdout.flush()
        imagereader.AndRed(and_image, self._images[file_name])

    majority_image = imagereader.MajorityRed(self._images)
    if True:
      print 'writing to xxx_majority.ppm'
      majority_image.save('xxx_majority.ppm', 'PPM')
    and_image = majority_image

    if or_image and and_image:
      # Combine or image and the and image, putting blue bull's eye at or_image
      result_pix_list = []
      for or_pix, and_pix in zip(or_image.getdata(), and_image.getdata()):
        if and_pix == imagereader.RED:
          result_pix = imagereader.BLUE
        else:
          result_pix = or_pix
        result_pix_list.append(result_pix)
      or_image.putdata(result_pix_list)

    if and_image:
      print 'writing xxx_and.ppm'
      and_image.save('xxx_and.ppm', 'PPM')
    if or_image:
      print 'writing xxx_or.ppm'
      or_image.save('xxx_or.ppm', 'PPM')

    if and_image:
      # Dump out guts of the function FixedWRFPoints.
      # Run this when the basic image changes.
      and_pix = and_image.load()
      print '  return ['
      for y in xrange(628):
        for x in xrange(628):
          if and_pix[x, y] == imagereader.RED:
            print '      (%3d, %3d),' % (x, y)
      print '  ]'

  def ExploreWindBarbs(self, file_name, output_file=sys.stdout):
    """Look for windbarbs in image taken from file_name.

    Args:
      file_name: The name of the file that the image was taken from.
      output_file: Where to write the CSV data created by the extraction.
    """

    # Apparently, we have to copy() the image before we can load() it
    image = self._images[file_name].copy()
    pix = image.load()

    wrapped_matrix = WrappedWRFPixMatrix(pix,
                                         imagereader.RED, imagereader.GREEN)
    blob_number = 0
    for x, y in FixedWRFPoints():
      start = Point(x, y)

      if False:
        # Development and then deprecated attempts.
        traversal = matrix.TraversalArtifact()
        matrix.ExploreLine(wrapped_matrix, 0, start, traversal)
        traversal.PrintTraversal()

        traversal.FindBestFittingLinesForEndPoints(
            '%03d' % (blob_number,), xscale=1.0, yscale=-1.0)

      # Operational code; does brute force best fit matching.
      # Rather slow.
      windbarb.ExploreWindBarb(
          wrapped_matrix,
          start,
          self._unix_epoch_time,
          attribution=file_name,
          output_file=output_file)

      blob_number += 1


def FixedWRFPoints():
  """Returns points where an "And" analysis gave points in common."""
  x_coords = [
      10, 34, 57, 80, 104,
      127, 150, 174, 197, 221,
      244, 267, 291, 314, 337,
      361, 384, 408, 431, 454,
      478, 501, 524, 548, 571,
      595, 618,
  ]
  y_coords = [
      10, 33, 57, 80, 104,
      127, 150, 174, 197, 220,
      244, 267, 291, 314, 337,
      361, 384, 407, 431, 454,
      477, 501, 524, 548, 571,
      594, 618,
  ]

  # Restrict the set of points returned.
  x_coords = x_coords[0:14]  # Empirically covers just the W. WA coast and JDF

  back = []
  for x in x_coords:
    for y in y_coords:
      back.append((x, y))
  return back


def ConvertUWPixelsToLatLon(p):
  """Convert a point, in pixels on the UW WRF 4Km Doman to LatLon."""
  # By measurements from the tick marks on the diagram.
  # The US/CDN border is at lat DM(49, 0)
  # y ==   0 ==> lat DM(49, 0) + 23km == DM(49, 12)
  # y == 628 ==> lat DM(46, 15)

  lat = DM(49, 12) - (p.y / 628.0 * abs((DM(49, 12) - DM(46, 15))))
  #
  # looking at the grid on the UW plot:
  # at the bottom DM(46,15), we have:
  # 80 ticks * 4km/tick = 320km = 172nm
  #  172/math.cos(math.radians(46.25)) = 248minutes lon = DM(4, 8)
  #
  # x ==   0 at bot    ==> -DM(125,  0)
  # x == 628 at bot    ==> -DM(120, 52)  # right edge assumed to be constant
  lon_bot = -DM(125, 00) + (p.x / 628.0 * abs(-DM(125, 00) - -DM(120, 52)))

  # at the top DM(49,12), we have:
  # 80 ticks * 4km/tick = 320km = 172nm
  #  172/math.cos(math.radians(49.20)) = 263minutes lon = DM(4, 23)
  # DM(125, 0) - DM(4, 8) = DM(120, 52)
  #
  # x ==   0 at top    ==> -DM(125, 15)  # 15 = 23 - 8 to compensate for barel
  # x == 628 at top    ==> -DM(120, 52)  # right edge assumed to be constant
  lon_top = -DM(125, 15) + (p.x / 628.0 * abs(-DM(125, 15) - -DM(120, 52)))

  # Now take a simple linear ratio based on the latitude
  lon = lon_top - (lon_top - lon_bot) * (p.y / 628)

  ll = LatLon(lat, lon)
  return ll


def main():
  for p in UWTodayDirectoryNames():
    print '%s' % (p,)
  print UWMostRecentCompleteModelRun()


if __name__ == '__main__':
  main()
